﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.Serialization;
using System.Xml.Linq;
using Loki.Utils;

namespace Luna.Connectors.ZohoCRM
{
    [Serializable]
    public abstract class ZohoEntity : LokiObject, ISerializable, IEnumerable<KeyValuePair<string, object>>
    {
        private readonly Dictionary<string, object> FIELDS = new Dictionary<string, object>();

        private const string OWNER_ID_FIELD_NAME = "SMOWNERID";
        private const string CREATOR_ID_FIELD_NAME = "SMCREATORID";

        protected ZohoEntity()
        {
            var L_Type = GetType();
            var L_Attribute = Attribute.GetCustomAttribute(L_Type, typeof(ModuleNameAttribute)) as ModuleNameAttribute;
            if (L_Attribute == null)
            {
                throw BuildErrorFormat<InvalidOperationException>(Resources.Errors.MISSING_MODULE_ATTRIBUTE, L_Type);
            }

            EntityName = L_Attribute.Name;
        }

        protected ZohoEntity(SerializationInfo P_Info, StreamingContext P_Context)
        {
            foreach (var L_Entry in P_Info)
            {
                FIELDS[L_Entry.Name] = L_Entry.Value;
            }
        }

        internal string EntityName { get; private set; }

        public string ID
        {
            get
            {
                var L_FieldName = IdFieldName;
                if (string.IsNullOrEmpty(L_FieldName))
                {
                    return null;
                }

                if (!HasValue(L_FieldName))
                {
                    return null;
                }

                return GetString(L_FieldName);
            }
        }

        protected virtual string IdFieldName
        {
            get { return null; }
        }

        public string OwnerId
        {
            get { return GetString(OWNER_ID_FIELD_NAME); }
        }

        public string CreatorId
        {
            get { return GetString(CREATOR_ID_FIELD_NAME); }
        }

        public bool HasValue(string P_FieldName)
        {
            return FIELDS.ContainsKey(P_FieldName);
        }

        public T GetValue<T>(string P_FieldName)
        {
            object L_Value;
            if (!FIELDS.TryGetValue(P_FieldName, out L_Value))
            {
                return default(T);
            }

            if (!(L_Value is T))
            {
                L_Value = Convert.ChangeType(L_Value, typeof(T));
            }

            return (T)L_Value;
        }

        public void SetValue<T>(string P_FieldName, T P_Value)
        {
            if (Equals(default(T), P_Value))
            {
                FIELDS.Remove(P_FieldName);
            }
            else if (!typeof(T).IsPrimitive && typeof(T) != typeof(decimal))
            {
                FIELDS[P_FieldName] = P_Value.ToString();
            }
            else
            {
                FIELDS[P_FieldName] = P_Value;
            }
        }

        public void SetValue(string P_FieldName, string P_Value)
        {
            SetValue<string>(P_FieldName, P_Value);
        }

        public void SetValue(string P_FieldName, byte P_Value)
        {
            SetValue<byte>(P_FieldName, P_Value);
        }

        [CLSCompliant(false)]
        public void SetValue(string P_FieldName, sbyte P_Value)
        {
            SetValue<sbyte>(P_FieldName, P_Value);
        }

        public void SetValue(string P_FieldName, short P_Value)
        {
            SetValue<short>(P_FieldName, P_Value);
        }

        [CLSCompliant(false)]
        public void SetValue(string P_FieldName, ushort P_Value)
        {
            SetValue<ushort>(P_FieldName, P_Value);
        }

        public void SetValue(string P_FieldName, int P_Value)
        {
            SetValue<int>(P_FieldName, P_Value);
        }

        [CLSCompliant(false)]
        public void SetValue(string P_FieldName, uint P_Value)
        {
            SetValue<uint>(P_FieldName, P_Value);
        }

        public void SetValue(string P_FieldName, long P_Value)
        {
            SetValue<long>(P_FieldName, P_Value);
        }

        [CLSCompliant(false)]
        public void SetValue(string P_FieldName, ulong P_Value)
        {
            SetValue<ulong>(P_FieldName, P_Value);
        }

        public void SetValue(string P_FieldName, float P_Value)
        {
            SetValue<float>(P_FieldName, P_Value);
        }

        public void SetValue(string P_FieldName, double P_Value)
        {
            SetValue<double>(P_FieldName, P_Value);
        }

        public void SetValue(string P_FieldName, decimal P_Value)
        {
            SetValue<decimal>(P_FieldName, P_Value);
        }

        public void SetValue(string P_FieldName, char P_Value)
        {
            SetValue<char>(P_FieldName, P_Value);
        }

        public void SetValue(string P_FieldName, DateTime P_Value)
        {
            SetValue<DateTime>(P_FieldName, P_Value);
        }

        public string GetString(string P_FieldName)
        {
            return GetValue<string>(P_FieldName);
        }

        public byte GetByte(string P_FieldName)
        {
            return GetValue<byte>(P_FieldName);
        }

        [CLSCompliant(false)]
        public sbyte GetSByte(string P_FieldName)
        {
            return GetValue<sbyte>(P_FieldName);
        }

        public short GetInt16(string P_FieldName)
        {
            return GetValue<short>(P_FieldName);
        }

        [CLSCompliant(false)]
        public ushort GetUInt16(string P_FieldName)
        {
            return GetValue<ushort>(P_FieldName);
        }

        public int GetInt32(string P_FieldName)
        {
            return GetValue<int>(P_FieldName);
        }

        [CLSCompliant(false)]
        public uint GetUInt32(string P_FieldName)
        {
            return GetValue<uint>(P_FieldName);
        }

        public long GetInt64(string P_FieldName)
        {
            return GetValue<long>(P_FieldName);
        }

        [CLSCompliant(false)]
        public ulong GetUInt64(string P_FieldName)
        {
            return GetValue<ulong>(P_FieldName);
        }

        public float GetSingle(string P_FieldName)
        {
            return GetValue<float>(P_FieldName);
        }

        public double GetDouble(string P_FieldName)
        {
            return GetValue<double>(P_FieldName);
        }

        public decimal GetDecimal(string P_FieldName)
        {
            return GetValue<decimal>(P_FieldName);
        }

        public DateTime GetDateTime(string P_FieldName)
        {
            return GetValue<DateTime>(P_FieldName);
        }

        public char GetChar(string P_FieldName)
        {
            return GetValue<char>(P_FieldName);
        }

        private static readonly string[] TRUE_VALUES = { "0", "false" };
        private static readonly string[] FALSE_VALUES = { "1", "true" };

        // boolean is a special case
        public bool GetBoolean(string P_FieldName)
        {
            object L_Value;
            if (!FIELDS.TryGetValue(P_FieldName, out L_Value))
            {
                return false;
            }

            if (L_Value is bool)
            {
                return (bool)L_Value;
            }

            string L_StringValue = L_Value as string;
            if (L_StringValue != null)
            {
                if (TRUE_VALUES.Contains(L_StringValue))
                {
                    return true;
                }

                if (FALSE_VALUES.Contains(L_StringValue))
                {
                    return false;
                }

                throw BuildError<FormatException>(Resources.Errors.INVALID_FORMAT_BOOLEAN);
            }

            return false;
        }

        internal virtual void AppendTo(XElement P_Parent, int P_RowNum = 1)
        {
            var L_Element = new XElement(SerializerParams.ELEMENT_ROW);
            L_Element.SetAttributeValue(SerializerParams.ATTRIBUTE_ROWNUM, P_RowNum);
            AppendFieldsToRow(L_Element);
            P_Parent.Add(L_Element);
        }

        internal virtual void AppendFieldsToRow(XElement P_RowElement)
        {
            foreach (var L_Field in FIELDS)
            {
                var L_FieldName = L_Field.Key;
                var L_IdFieldName = IdFieldName;
                if (!string.IsNullOrEmpty(L_IdFieldName) && L_IdFieldName.Equals(L_FieldName))
                {
                    continue;
                }

                if (L_FieldName.Equals(CREATOR_ID_FIELD_NAME))
                {
                    continue;
                }

                var L_Value = L_Field.Value;
                if (L_Value == null)
                {
                    continue;
                }

                var L_FieldElement = new XElement(SerializerParams.ELEMENT_FIELD);
                L_FieldElement.SetAttributeValue(SerializerParams.ATTRIBUTE_VALUE, L_Field.Key);

                string L_StringValue = L_Value as string;
                if (L_StringValue == null)
                {
                    L_StringValue = Convert.ToString(L_Value, CultureInfo.InvariantCulture);
                    L_FieldElement.Add(L_StringValue);
                }
                else
                {
                    // for the moment we always use CDATA to avoid errors given by invalid XML
                    // strings... in a later version we'll map this against the field type ...
                    L_FieldElement.Add(new XCData(L_StringValue));
                }

                P_RowElement.Add(L_FieldElement);
            }
        }

        public XElement ToXml()
        {
            var L_Root = new XElement(EntityName);
            AppendTo(L_Root);
            return L_Root;
        }

        internal virtual void LoadFromXml(XElement P_Element)
        {
            var L_FieldNodes = P_Element.Descendants();
            foreach (var L_Node in L_FieldNodes)
            {
                LoadFieldFromXml(L_Node);
            }
        }

        internal virtual void LoadFieldFromXml(XElement P_FieldElement)
        {
            var L_Key = P_FieldElement.Attribute(SerializerParams.ATTRIBUTE_VALUE);
            var L_Value = P_FieldElement.Nodes().OfType<XText>().FirstOrDefault();

            if (L_Key == null)
            {
                throw BuildError<FormatException>(Resources.Errors.INVALID_FORMAT_FIELD_KEY);
            }

            if (L_Value == null)
            {
                throw BuildError<FormatException>(Resources.Errors.INVALID_FORMAT_FIELD_VALUE);
            }

            SetValue(L_Key.Value, L_Value.Value);
        }

        public virtual void GetObjectData(SerializationInfo P_Info, StreamingContext P_Context)
        {
            foreach (var P_Field in FIELDS)
            {
                P_Info.AddValue(P_Field.Key, P_Field.Value);
            }
        }

        public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
        {
            lock (FIELDS)
            {
                return FIELDS.GetEnumerator();
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}